﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Text;
using System.IO;
using System.Reflection;

namespace JmrToLinqToSqlGenerator
{
	public class JmrToLinqToSqlGenerator
	{
		public JmrToLinqToSqlGenerator() { }

		public void Generate(string dbmlPath, string dbmlCsPath, string outputPath, string csproj, bool makeSerializable, string version)
		{
			AssimilateDbmlCs(dbmlCsPath, makeSerializable);

			Console.WriteLine("Generating entities file: {0}", outputPath);

			var sb = new StringBuilder();
			var meta = new DbmlReader().Load(dbmlPath);

			BeginPragmaDisableSection(sb);
			FormatAutoGeneratedSection(sb, version);
			sb.AppendLine();

			FormatUsingSection(sb, csproj);
			sb.AppendLine();
			
			BeginNamespaceSection(sb, meta);

			foreach (var entityMeta in meta.Entities)
			{
				FormatClassSection(sb, entityMeta, meta, makeSerializable);
				sb.AppendLine();
				FormatCollectionSection(sb, entityMeta, meta, makeSerializable);
				sb.AppendLine();
			}

			foreach (var func in meta.Functions)
			{
				FormatFunctionClassSection(sb, func, meta, makeSerializable);
				sb.AppendLine();
				FormatFunctionCollectionSection(sb, func, meta, makeSerializable);
				sb.AppendLine();
			}

			EndNamespaceSection(sb);
			EndPragmaDisableSection(sb);

			#if (DEBUG)
			Console.WriteLine(sb.ToString());
			Console.ReadLine();
			#endif

			var sw = new StreamWriter(outputPath, false);
			sw.Write(sb.ToString());
			sw.Close();
			sw = null;

			Console.WriteLine("Succesfully generated business entities file.");
		}

		private void AssimilateDbmlCs(string dbmlCsPath, bool makeSerializable)
		{
			if (dbmlCsPath == null) { return; }
			Console.WriteLine("Modifying file: {0}", dbmlCsPath);

			var stream = File.OpenText(dbmlCsPath);
			var sb = new StringBuilder(stream.ReadToEnd());
			stream.Dispose();

			sb.Replace("[Table(", "[Serializable, Table(");
			sb.Replace(" : INotifyPropertyChanging, INotifyPropertyChanged", string.Empty);

			sb.Replace(", UpdateCheck=UpdateCheck.Always", string.Empty);
			sb.Replace("UpdateCheck=UpdateCheck.Always, ", string.Empty);

			sb.Replace(", UpdateCheck=UpdateCheck.WhenChanged", string.Empty);
			sb.Replace("UpdateCheck=UpdateCheck.WhenChanged, ", string.Empty);

			sb.Replace(", UpdateCheck=UpdateCheck.Never", string.Empty);
			sb.Replace("UpdateCheck=UpdateCheck.Never, ", string.Empty);

			sb.Replace("[Column(", "[Column(UpdateCheck=UpdateCheck.Never, ");

			var sw = new StreamWriter(dbmlCsPath, false);
			sw.Write(sb.ToString());
			sw.Close();
			sw = null;

			Console.WriteLine("Succesfully modified file.");
		}

		private void BeginPragmaDisableSection(StringBuilder sb)
		{
			sb.AppendLine("#pragma warning disable 1591");
		}

		private void EndPragmaDisableSection(StringBuilder sb)
		{
			sb.AppendLine("#pragma warning restore 1591");
		}

		private void FormatAutoGeneratedSection(StringBuilder sb, string version)
		{
			sb.AppendLine("//------------------------------------------------------------------------------");
			sb.AppendLine("// <auto-generated>");
			sb.AppendLine("//     This code was generated by the JmrToLinqToSqlGenerator tool.");
			sb.AppendLine(string.Format("//     JmrToLinqToSqlGenerator Tool Version:{0}", version));
			sb.AppendLine("//");
			sb.AppendLine("//     Changes to this file may cause incorrect behavior and will be lost if");
			sb.AppendLine("//     the code is regenerated.");
			sb.AppendLine("// </auto-generated>");
			sb.AppendLine("//------------------------------------------------------------------------------");
		}

		private void FormatUsingSection(StringBuilder sb, string csproj)
		{
			sb.AppendLine("using System;");
			sb.AppendLine("using System.Linq;");
			sb.AppendLine("using System.Collections.Generic;");
			sb.AppendLine("using System.Collections.ObjectModel;");
			sb.AppendLine("using Jmr.Entity35;");
			sb.AppendLine("using Jmr.Entity35.Validation;");
			
			if (!string.IsNullOrEmpty(csproj))
				sb.AppendLine(string.Format("using {0};", csproj.Substring(0, csproj.IndexOf("."))));
		}

		private void BeginNamespaceSection(StringBuilder sb, ContextMeta meta)
		{
			sb.AppendLine(string.Format("namespace {0}.Business", meta.DatabaseName));
			sb.AppendLine("{");
		}

		private void EndNamespaceSection(StringBuilder sb)
		{
			sb.AppendLine("}");
		}

		private void FormatClassSection(StringBuilder sb, EntityMeta entityMeta, ContextMeta meta, bool makeSerializable)
		{
			Console.Write("Generating {0} business entity class...", entityMeta.JmrBusinessClassName);

			if (makeSerializable)
				sb.AppendLine("\t[Serializable]");

			sb.AppendLine(string.Format("\tpublic partial class {0} : IEntity, IValidateable", entityMeta.JmrBusinessClassName));
			sb.AppendLine("\t{");
			
			FormatClassConstructorSection(sb, entityMeta);
			sb.AppendLine();
			FormatClassContextSection(sb, meta);
			sb.AppendLine();
			FormatLinqEntitySection(sb, entityMeta);

			sb.AppendLine();
			sb.AppendLine("\t\t#region Properties");
			sb.AppendLine();
			foreach (var propMeta in entityMeta.Properties)
			{
				FormatClassPropertySection(sb, propMeta, true);
				sb.AppendLine();
			}
			sb.AppendLine();
			sb.AppendLine("\t\t#endregion");
			sb.AppendLine();
			
			sb.AppendLine("\t\t#region Associations");
			sb.AppendLine();
			foreach (var propMeta in entityMeta.Associations)
			{
				FormatClassAssociationSection(sb, propMeta);
				sb.AppendLine();
			}
			sb.AppendLine();
			sb.AppendLine("\t\t#endregion");
			sb.AppendLine();
			
			FormatIEntitySection(sb, entityMeta);
			sb.AppendLine();
			
			FormatIValidateableSection(sb);
			sb.AppendLine("\t}");
			
			Console.WriteLine("Done!");
		}

		private void FormatClassConstructorSection(StringBuilder sb, EntityMeta meta)
		{
			sb.AppendLine(string.Format("\t\tpublic {0}()", meta.JmrBusinessClassName));
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tValidators.Add(new SimpleValidator());");
			sb.AppendLine("\t\t}");
		}

		private void FormatClassContextSection(StringBuilder sb, ContextMeta meta)
		{
			sb.AppendLine("\t\t[NonSerialized]");
			sb.AppendLine(string.Format("\t\tprivate {0} _ctx = null;", meta.ContextClassName));
			sb.AppendLine(string.Format("\t\tpublic {0} Context", meta.ContextClassName));
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tget");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\tif (_ctx == null)");
			sb.AppendLine(string.Format("\t\t\t\t\t_ctx = new {0}();", meta.ContextClassName));
			sb.AppendLine();
			sb.AppendLine("\t\t\t\treturn _ctx;");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t\tset");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\t_ctx = value;");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t}");
		}

		private void FormatLinqEntitySection(StringBuilder sb, EntityMeta meta)
		{
			sb.AppendLine(string.Format("\t\tprivate {0} _linqEntity = null;", meta.LinqEntityClassName));
			sb.AppendLine(string.Format("\t\tinternal {0} LinqEntity", meta.LinqEntityClassName));
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tget");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\tif (_linqEntity == null)");
			sb.AppendLine(string.Format("\t\t\t\t\t_linqEntity = new {0}();", meta.LinqEntityClassName));
			sb.AppendLine();
			sb.AppendLine("\t\t\t\treturn _linqEntity;");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t\tset");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\t_linqEntity = value;");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t}");
		}

		private void FormatClassPropertySection(StringBuilder sb, EntityPropertyMeta meta, bool implementHasChanges)
		{
			sb.AppendLine(string.Format("\t\tpublic virtual {0} {1}", meta.ClrType, meta.JmrPropertyName));
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tget");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine(string.Format("\t\t\t\treturn LinqEntity.{0};", meta.LinqPropertyName));
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t\tset");
			sb.AppendLine("\t\t\t{");
			
			if (implementHasChanges)
				sb.AppendLine(string.Format("\t\t\t\tHasChanges = (LinqEntity.{0} != value);", meta.LinqPropertyName));
			
			sb.AppendLine(string.Format("\t\t\t\tLinqEntity.{0} = value;", meta.LinqPropertyName));
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t}");
		}

		private void FormatClassAssociationSection(StringBuilder sb, EntityAssociationMeta meta)
		{
			if (meta.IsParent)
			{
				sb.AppendLine(string.Format("\t\tpublic virtual {0}Collection {1}", meta.JmrEntityName, meta.JmrCollectionMemberName));
				sb.AppendLine("\t\t{");
				sb.AppendLine("\t\t\tget");
				sb.AppendLine("\t\t\t{");
				sb.AppendLine(string.Format("\t\t\t\tif (_{0} == null)", meta.JmrCollectionMemberName));
				sb.AppendLine(string.Format("\t\t\t\t\t_{0} = new {1}Collection();", meta.JmrCollectionMemberName, meta.JmrEntityName));
				sb.AppendLine();
				sb.AppendLine(string.Format("\t\t\t\tforeach (var item in LinqEntity.{0})", meta.LinqCollectionMemberName));
				sb.AppendLine("\t\t\t\t{");
				sb.AppendLine(string.Format("\t\t\t\t\tvar entity = new {0}();", meta.JmrEntityName));
				sb.AppendLine("\t\t\t\t\tentity.LinqEntity = item;");
				sb.AppendLine(string.Format("\t\t\t\t\t_{0}.Add(entity);", meta.JmrCollectionMemberName));
				sb.AppendLine("\t\t\t\t}");
				sb.AppendLine();
				sb.AppendLine(string.Format("\t\t\t\treturn _{0};", meta.JmrCollectionMemberName));
				sb.AppendLine("\t\t\t}");
				sb.AppendLine("\t\t}");
				sb.AppendLine(string.Format("\t\tprivate {0}Collection _{1} = null;", meta.JmrEntityName, meta.JmrCollectionMemberName));
			}
			else
			{
				sb.AppendLine(string.Format("\t\tpublic virtual {0} {0}", meta.JmrEntityName));
				sb.AppendLine("\t\t{");
				sb.AppendLine("\t\t\tget");
				sb.AppendLine("\t\t\t{");
				sb.AppendLine(string.Format("\t\t\t\tif (_{0} == null)", meta.JmrEntityName));
				sb.AppendLine(string.Format("\t\t\t\t\t_{0} = new {0}();", meta.JmrEntityName));
				sb.AppendLine();
				sb.AppendLine(string.Format("\t\t\t\t_{0}.LinqEntity = LinqEntity.{1};", meta.JmrEntityName, meta.LinqEntityName));
				sb.AppendLine(string.Format("\t\t\t\treturn _{0};", meta.JmrEntityName));
				sb.AppendLine("\t\t\t}");
				sb.AppendLine("\t\t}");
				sb.AppendLine(string.Format("\t\tprivate {0} _{0} = null;", meta.JmrEntityName));
			}
		}

		private void FormatIEntitySection(StringBuilder sb, EntityMeta entityMeta)
		{
			var keyProps = from prop in entityMeta.Properties
										 where prop.IsKey
										 select prop;

			sb.AppendLine("\t\t#region IEntity Members");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic bool HasChanges { get; internal set; }");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic bool LoadedFromDb { get; internal set; }");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic event EntityEventHander OnBeforeLoad;");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic event EntityEventHander OnAfterLoad;");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic event EntityEventHander OnBeforeSave;");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic event EntityEventHander OnAfterSave;");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic event EntityEventHander OnBeforeDelete;");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic event EntityEventHander OnAfterDelete;");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic virtual void Load()");
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tif (OnBeforeLoad != null) { OnBeforeLoad(this); }");
			sb.AppendLine();

			if (keyProps.Count() > 0)
			{
				sb.AppendLine(string.Format("\t\t\tLinqEntity = (from t in Context.{0}", entityMeta.LinqEntityCollectionName));
				sb.AppendLine("\t\t\t\twhere");
				var whereClause = string.Empty;

				foreach (var prop in keyProps)
					whereClause += string.Format("t.{0} == {1} && ", prop.LinqPropertyName, prop.JmrPropertyName);
				
				sb.AppendLine("\t\t\t\t\t" + whereClause.Substring(0, whereClause.Length-4));
				sb.AppendLine("\t\t\t\tselect t).Single();");
				sb.AppendLine("\t\t\tLoadedFromDb = true;");
			}
			else
			{
				sb.AppendLine("\t\t\tthrow new System.Exception(\"JmrToLinqToSqlGenerator could not implement this method.  Missing primary key column.\");");
			}

			sb.AppendLine();
			sb.AppendLine("\t\t\tif (OnAfterLoad != null) { OnAfterLoad(this); }");
			sb.AppendLine("\t\t}");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic virtual void Save()");
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tif (OnBeforeSave != null) { OnBeforeSave(this); }");
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\t\tContext.{0}.Attach(LinqEntity, HasChanges);", entityMeta.LinqEntityCollectionName));
			sb.AppendLine("\t\t\tContext.SubmitChanges();");
			sb.AppendLine();
			sb.AppendLine("\t\t\tif (OnAfterSave != null) { OnAfterSave(this); }");
			sb.AppendLine("\t\t}");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic virtual void Delete()");
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tif (OnBeforeDelete != null) { OnBeforeDelete(this); }");
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\t\tContext.{0}.DeleteOnSubmit(LinqEntity);", entityMeta.LinqEntityCollectionName));
			sb.AppendLine("\t\t\tContext.SubmitChanges();");
			sb.AppendLine();
			sb.AppendLine("\t\t\tif (OnAfterDelete != null) { OnAfterDelete(this); }");
			sb.AppendLine("\t\t}");
			sb.AppendLine();
			sb.AppendLine("\t\t#endregion");
		}

		private void FormatIValidateableSection(StringBuilder sb)
		{
			sb.AppendLine("\t\t#region IValidateable Members");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic void ClearValidationErrors()");
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tValidationErrors.Clear();");
			sb.AppendLine("\t\t}");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic bool HasValidationErrors");
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tget { return (ValidationErrors.Count > 0); }");
			sb.AppendLine("\t\t}");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic List<IValidator> Validators");
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tget { return _validators; }");
			sb.AppendLine("\t\t}");
			sb.AppendLine("\t\tprivate List<IValidator> _validators = new List<IValidator>();");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic void Validate()");
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tforeach (var v in Validators)");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\tv.Validate(this);");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine();
			sb.AppendLine("\t\t\tif (HasValidationErrors)");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\tvar errorMessages = string.Empty;");
			sb.AppendLine();
			sb.AppendLine("\t\t\t\tforeach (var v in ValidationErrors)");
			sb.AppendLine("\t\t\t\t\terrorMessages += string.Format(\"{0}\", v.GetValidationErrorMessage());");
			sb.AppendLine();
			sb.AppendLine("\t\t\t\tif (ValidationErrorHandling == ValidationErrorHandling.ThrowException)");
			sb.AppendLine("\t\t\t\t\tthrow new ValidationException(errorMessages);");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t}");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic ValidationErrorHandling ValidationErrorHandling");
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tget");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\treturn _validationErrorHandling;");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t\tset");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\t_validationErrorHandling = value;");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t}");
			sb.AppendLine("\t\tprivate ValidationErrorHandling _validationErrorHandling = ValidationErrorHandling.ThrowException;");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic List<IValidationError> ValidationErrors");
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tget { return _validationErrors; }");
			sb.AppendLine("\t\t}");
			sb.AppendLine("\t\tprivate List<IValidationError> _validationErrors = new List<IValidationError>();");
			sb.AppendLine();
			sb.AppendLine("\t\t#endregion");
		}

		private void FormatCollectionSection(StringBuilder sb, EntityMeta entityMeta, ContextMeta meta, bool makeSerializable)
		{
			Console.Write("Generating {0}Collection business entity collection class...", entityMeta.JmrBusinessClassName);
			
			if (makeSerializable)
				sb.AppendLine("\t[Serializable]");
			
			sb.AppendLine(string.Format("\tpublic partial class {0}Collection : Collection<{0}>, IEntityCollection<{0}>", entityMeta.JmrBusinessClassName));
			sb.AppendLine("\t{");
			sb.AppendLine(string.Format("\t\tpublic {0}Collection() {{ }}", entityMeta.JmrBusinessClassName));
			sb.AppendLine();
			FormatClassContextSection(sb, meta);
			sb.AppendLine();
			FormatIEntityCollectionSection(sb, entityMeta);
			sb.AppendLine("\t}");

			Console.WriteLine("Done!");
		}

		private void FormatIEntityCollectionSection(StringBuilder sb, EntityMeta meta)
		{
			sb.AppendLine(string.Format("\t\t#region IEntityCollection<{0}> Members", meta.JmrBusinessClassName));
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\tpublic event EntityCollectionEventHander<{0}> OnBeforeLoad;", meta.JmrBusinessClassName));
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\tpublic event EntityCollectionEventHander<{0}> OnAfterLoad;", meta.JmrBusinessClassName));
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\tpublic event EntityCollectionEventHander<{0}> OnBeforeSave;", meta.JmrBusinessClassName));
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\tpublic event EntityCollectionEventHander<{0}> OnAfterSave;", meta.JmrBusinessClassName));
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\tpublic event EntityCollectionEventHander<{0}> OnBeforeDelete;", meta.JmrBusinessClassName));
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\tpublic event EntityCollectionEventHander<{0}> OnAfterDelete;", meta.JmrBusinessClassName));
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\tpublic virtual void AddRange(IEnumerable<{0}> enumerable)", meta.JmrBusinessClassName));
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tforeach (var item in enumerable)");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\tAdd(item);");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t}");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic virtual void Load()");
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tif (OnBeforeLoad != null) { OnBeforeLoad(this); }");
			sb.AppendLine(string.Format("\t\t\tforeach (var linqEntity in Context.{0})", meta.LinqEntityCollectionName));
			sb.AppendLine("\t\t\t{");
			sb.AppendLine(string.Format("\t\t\t\tvar entity = new {0}();", meta.JmrBusinessClassName));
			sb.AppendLine("\t\t\t\tentity.LinqEntity = linqEntity;");
			sb.AppendLine("\t\t\t\tAdd(entity);");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t\tif (OnAfterLoad != null) { OnAfterLoad(this); }");
			sb.AppendLine("\t\t}");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic virtual void Save()");
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tif (OnBeforeSave != null) { OnBeforeSave(this); }");
			sb.AppendLine();
			sb.AppendLine("\t\t\tvar updates = from item in this");
			sb.AppendLine("\t\t\t\twhere item.HasChanges && item.LoadedFromDb");
			sb.AppendLine("\t\t\t\tselect new { BusEntity = item, LinqEntity = item.LinqEntity };");
			sb.AppendLine();
			sb.AppendLine("\t\t\tvar inserts = from item in this");
			sb.AppendLine("\t\t\t\twhere item.LoadedFromDb");
			sb.AppendLine("\t\t\t\tselect new { BusEntity = item, LinqEntity = item.LinqEntity };");
			sb.AppendLine();
			sb.AppendLine("\t\t\tforeach (var item in updates)");
			sb.AppendLine("\t\t\t\tif (item.BusEntity is IValidateable) { ((IValidateable)item.BusEntity).Validate(); }");
			sb.AppendLine();
			sb.AppendLine("\t\t\tforeach (var item in inserts)");
			sb.AppendLine("\t\t\t\tif (item.BusEntity is IValidateable) { ((IValidateable)item.BusEntity).Validate(); }");
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\t\tContext.{0}.AttachAll(updates.Select(i => i.LinqEntity), true);", meta.LinqEntityCollectionName));
			sb.AppendLine(string.Format("\t\t\tContext.{0}.InsertAllOnSubmit(inserts.Select(i => i.LinqEntity));", meta.LinqEntityCollectionName));
			sb.AppendLine("\t\t\tContext.SubmitChanges();");
			sb.AppendLine();
			sb.AppendLine("\t\t\tif (OnAfterSave != null) { OnAfterSave(this); }");
			sb.AppendLine("\t\t}");
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\tpublic virtual void Delete({0} entity)", meta.JmrBusinessClassName));
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tif (OnBeforeDelete != null) { OnBeforeDelete(this); }");
			sb.AppendLine();
			sb.AppendLine("\t\t\tthis[IndexOf(entity)].Delete();");
			sb.AppendLine();
			sb.AppendLine("\t\t\tif (OnAfterDelete != null) { OnAfterDelete(this); }");
			sb.AppendLine("\t\t}");
			sb.AppendLine();
			sb.AppendLine("\t\tpublic virtual void DeleteAt(int index)");
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tif (OnBeforeDelete != null) { OnBeforeDelete(this); }");
			sb.AppendLine();
			sb.AppendLine("\t\t\tthis[index].Delete();");
			sb.AppendLine();
			sb.AppendLine("\t\t\tif (OnAfterDelete != null) { OnAfterDelete(this); }");
			sb.AppendLine("\t\t}");
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\tpublic virtual void DeleteRange(IEnumerable<{0}> enumerable)", meta.JmrBusinessClassName));
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tif (OnBeforeDelete != null) { OnBeforeDelete(this); }");
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\t\tContext.{0}.DeleteAllOnSubmit(this.Select(i => i.LinqEntity));", meta.LinqEntityCollectionName));
			sb.AppendLine("\t\t\tContext.SubmitChanges();");
			sb.AppendLine();
			sb.AppendLine("\t\t\tif (OnAfterDelete != null) { OnAfterDelete(this); }");
			sb.AppendLine("\t\t}");
			sb.AppendLine();
			sb.AppendLine("\t\t#endregion");
		}

		private void FormatFunctionClassSection(StringBuilder sb, FunctionMeta funcMeta, ContextMeta contextMeta, bool makeSerializable)
		{
			Console.Write("Generating {0} business entity class...", funcMeta.JmrElementTypeName);
			
			if (!string.IsNullOrEmpty(funcMeta.LinqEntityIdRef)) { return; }
			
			if (makeSerializable)
				sb.AppendLine("\t[Serializable]");

			sb.AppendLine(string.Format("\tpublic partial class {0}", funcMeta.JmrElementTypeName));
			sb.AppendLine("\t{");
			sb.AppendLine(string.Format("\t\tpublic {0}() {{ }}", funcMeta.JmrElementTypeName));
			sb.AppendLine();
			sb.AppendLine(string.Format("\t\tprivate {0} _linqEntity = null;", funcMeta.LinqElementTypeName));
			sb.AppendLine(string.Format("\t\tinternal {0} LinqEntity", funcMeta.LinqElementTypeName));
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tget");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\tif (_linqEntity == null)");
			sb.AppendLine(string.Format("\t\t\t\t\t_linqEntity = new {0}();", funcMeta.LinqElementTypeName));
			sb.AppendLine();
			sb.AppendLine("\t\t\t\treturn _linqEntity;");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t\tset");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\t_linqEntity = value;");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t}");
			sb.AppendLine();

			foreach (var prop in funcMeta.Properties)
			{
				FormatClassPropertySection(sb, prop, false);
				sb.AppendLine();
			}

			sb.AppendLine("\t}");

			Console.WriteLine("Done!");
		}

		private void FormatFunctionCollectionSection(StringBuilder sb, FunctionMeta funcMeta, ContextMeta contextMeta, bool makeSerializable)
		{
			Console.Write("Generating {0} business entity class...", funcMeta.JmrMethodName);
			
			if (!string.IsNullOrEmpty(funcMeta.LinqEntityIdRef)) 
			{
				funcMeta.JmrElementTypeName = (from e in contextMeta.Entities
																			where e.LinqIdRef == funcMeta.LinqEntityIdRef
																			select e).Single().JmrBusinessClassName;
			}
			
			if (makeSerializable)
				sb.AppendLine("\t[Serializable]");

			sb.AppendLine(string.Format("\tpublic partial class {0} : Collection<{1}>", funcMeta.JmrMethodName, funcMeta.JmrElementTypeName));
			sb.AppendLine("\t{");
			sb.AppendLine(string.Format("\t\tpublic {0}() {{ }}", funcMeta.JmrMethodName));
			sb.AppendLine();
			sb.AppendLine("\t\t[NonSerialized]");
			sb.AppendLine(string.Format("\t\tprivate {0} _ctx = null;", contextMeta.ContextClassName));
			sb.AppendLine(string.Format("\t\tpublic {0} Context", contextMeta.ContextClassName));
			sb.AppendLine("\t\t{");
			sb.AppendLine("\t\t\tget");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\tif (_ctx == null)");
			sb.AppendLine(string.Format("\t\t\t\t\t_ctx = new {0}();", contextMeta.ContextClassName));
			sb.AppendLine();
			sb.AppendLine("\t\t\t\treturn _ctx;");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t\tset");
			sb.AppendLine("\t\t\t{");
			sb.AppendLine("\t\t\t\t_ctx = value;");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t}");
			sb.AppendLine();

			var parmSignatureString = string.Empty;
			var parmRunString = string.Empty;
			foreach (var parm in funcMeta.Parameters)
			{
				parmSignatureString += string.Format("{0} {1}, ", parm.ClrType, parm.JmrParameterName);
				parmRunString += string.Format("{0}, ", parm.JmrParameterName);
			}

			if (!string.IsNullOrEmpty(parmSignatureString))
				parmSignatureString = parmSignatureString.Substring(0, parmSignatureString.Length - 2);
			if (!string.IsNullOrEmpty(parmRunString))
				parmRunString = parmRunString.Substring(0, parmRunString.Length - 2);

			sb.AppendLine(string.Format("\t\tpublic virtual void Load({0})", parmSignatureString));
			sb.AppendLine("\t\t{");
			sb.AppendLine(string.Format("\t\t\tforeach (var linqEntity in Context.{0}({1}))", funcMeta.LinqMethodName, parmRunString));
			sb.AppendLine("\t\t\t{");
			sb.AppendLine(string.Format("\t\t\t\tvar entity = new {0}();", funcMeta.JmrElementTypeName));
			sb.AppendLine("\t\t\t\tentity.LinqEntity = linqEntity;");
			sb.AppendLine("\t\t\t\tAdd(entity);");
			sb.AppendLine("\t\t\t}");
			sb.AppendLine("\t\t}");
			sb.AppendLine("\t}");

			Console.WriteLine("Done!");
		}
	}

	public class DbmlReader
	{
		public DbmlReader() { }

		public ContextMeta Load(string dbmlPath)
		{
			var ret = new ContextMeta();
			var dbml = XDocument.Load(dbmlPath);
			var databaseElement = dbml.Root;
			var tableElements = databaseElement.Elements().Where(e => e.Name.LocalName == "Table");
			var functionElements = databaseElement.Elements().Where(e => e.Name.LocalName == "Function");

			ret.ContextClassName = databaseElement.Attribute("Class").Value;
			ret.DatabaseName = databaseElement.Attribute("Name").Value;

			foreach (var table in tableElements)
			{
				var entityMeta = new EntityMeta();
				var linqEntityClassName = table.Elements().Where(e => e.Name.LocalName == "Type").Single().Attribute("Name").Value;
				var idRefAttribute = table.Elements().Where(e => e.Name.LocalName == "Type").Single().Attribute("Id");
				var idRef = (idRefAttribute != null) ? idRefAttribute.Value : null;

				var busEntityClassName = string.Empty;
				if (linqEntityClassName.Contains("_"))
					busEntityClassName = linqEntityClassName.Replace((linqEntityClassName.Substring(0, linqEntityClassName.IndexOf("_") + 1)), string.Empty);
				else
					busEntityClassName = linqEntityClassName + "Bus";

				var linqEntityCollectionName = table.Attribute("Member").Value;
				var entityColumns = table.Elements().Where(e => e.Name.LocalName == "Type").Elements().Where(e => e.Name.LocalName == "Column");
				var entityAssociations = table.Elements().Where(e => e.Name.LocalName == "Type").Elements().Where(e => e.Name.LocalName == "Association");

				entityMeta.JmrBusinessClassName = busEntityClassName;
				entityMeta.LinqEntityClassName = linqEntityClassName;
				entityMeta.LinqEntityCollectionName = linqEntityCollectionName;
				entityMeta.LinqIdRef = idRef;

				foreach (var column in entityColumns)
				{
					var columnName = column.Attribute("Name").Value;
					var isKey = (column.Attribute("IsPrimaryKey") != null) ? Convert.ToBoolean(column.Attribute("IsPrimaryKey").Value) : false;
					var propertyName = (isKey && columnName.Contains("_")) ? columnName.Replace(columnName.Substring(0, columnName.IndexOf("_") + 1), string.Empty) : columnName.Replace("_", string.Empty);
					var clrType = column.Attribute("Type").Value;
					var isNullable = Convert.ToBoolean(column.Attribute("CanBeNull").Value);
					var entityProperty = new EntityPropertyMeta();

					columnName = columnName.Replace("[", "_").Replace("]", string.Empty);
					propertyName = propertyName.Replace("[", "P_").Replace("]", string.Empty);

					entityProperty.ClrType = clrType;
					entityProperty.LinqPropertyName = columnName;
					entityProperty.IsNullable = isNullable;
					entityProperty.JmrPropertyName = propertyName.Substring(0, 1).ToUpper() + propertyName.Substring(1, propertyName.Length - 1);
					entityProperty.IsKey = isKey;

					TranslateClrType(entityProperty);
					entityMeta.Properties.Add(entityProperty);
				}

				foreach (var assoc in entityAssociations)
				{
					var memberName = assoc.Attribute("Member").Value;
					var keyName = (assoc.Attribute("OtherKey") != null) ? assoc.Attribute("OtherKey").Value : assoc.Attribute("ThisKey").Value;
					var typeName = assoc.Attribute("Type").Value;
					var isParent = (assoc.Attribute("IsForeignKey") != null) ? !Convert.ToBoolean(assoc.Attribute("IsForeignKey").Value) : true;
					var entityAssoc = new EntityAssociationMeta();

					entityAssoc.LinqCollectionMemberName = memberName;
					
					if (memberName.Contains("_"))
						entityAssoc.JmrCollectionMemberName = memberName.Replace(memberName.Substring(0, memberName.IndexOf("_") + 1), string.Empty);
					else
						entityAssoc.JmrCollectionMemberName = memberName;

					entityAssoc.LinqEntityName = typeName;

					if (memberName.Contains("_"))
						entityAssoc.JmrEntityName = typeName.Replace(typeName.Substring(0, typeName.IndexOf("_") + 1), string.Empty);
					else
						entityAssoc.JmrEntityName = typeName;
					
					entityAssoc.LinqEntityKeyPropertyName = keyName;
					entityAssoc.IsParent = isParent;

					entityMeta.Associations.Add(entityAssoc);
				}
				ret.Entities.Add(entityMeta);
			}

			foreach (var func in functionElements)
			{
				var functionMeta = new FunctionMeta();
				var parameters = func.Elements().Where(e => e.Name.LocalName == "Parameter");
				var elementTypes = func.Elements().Where(e => e.Name.LocalName == "ElementType");
				var returnTypes = func.Elements().Where(e => e.Name.LocalName == "Return");
				var elementTypeAttrib = (elementTypes.Count() == 1) ? elementTypes.Single().Attribute("Name") : null;
				var elementTypeName = (elementTypeAttrib != null) ? elementTypeAttrib.Value : null;

				if (elementTypes.Count() != 1) { continue; }

				var columns = (elementTypes.Count() == 1) ? elementTypes.Single().Elements().Where(e => e.Name.LocalName == "Column") : null;
				var returnType = (returnTypes.Count() == 1) ?
					returnTypes.Single().Attribute("Type").Value : null;
				var linqMethodName = func.Attribute("Method").Value;
				var linqEntityIdRefAttrib = (elementTypes.Count() == 1) ? elementTypes.Single().Attribute("IdRef") : null;
				var linqEntityIdRef = (linqEntityIdRefAttrib != null) ? linqEntityIdRefAttrib.Value : null;
				var jmrMethodName = linqMethodName.Replace("_", string.Empty);
				jmrMethodName = jmrMethodName.Substring(0, 1).ToUpper() + jmrMethodName.Substring(1, jmrMethodName.Length - 1);
				var jmrElementTypeName = (elementTypeName != null) ? elementTypeName.Replace("_", string.Empty) : null;
				jmrElementTypeName = (jmrElementTypeName != null) ? jmrElementTypeName.Substring(0, 1).ToUpper() + jmrElementTypeName.Substring(1, jmrElementTypeName.Length - 1) : null;

				functionMeta.LinqMethodName = linqMethodName;
				functionMeta.JmrMethodName = jmrMethodName;
				functionMeta.LinqElementTypeName = elementTypeName;
				functionMeta.JmrElementTypeName = jmrElementTypeName;
				functionMeta.ClrReturnType = returnType;
				
				functionMeta.LinqEntityIdRef = linqEntityIdRef;

				foreach (var parm in parameters)
				{
					var parmName = parm.Attribute("Name").Value;
					var parmType = parm.Attribute("Type").Value;

					var parameterMeta = new FunctionParameterMeta();

					parameterMeta.LinqParameterName = parmName;
					parameterMeta.JmrParameterName = parmName.Replace("_", string.Empty);
					parameterMeta.ClrType = parmType;
					parameterMeta.IsNullable = true;

					TranslateClrType(parameterMeta);
					functionMeta.Parameters.Add(parameterMeta);
				}

				foreach (var column in columns)
				{
					var columnName = column.Attribute("Name").Value;
					var isKey = (column.Attribute("IsPrimaryKey") != null) ? Convert.ToBoolean(column.Attribute("IsPrimaryKey").Value) : false;
					var propertyName = (isKey && columnName.Contains("_")) ? columnName.Replace(columnName.Substring(0, columnName.IndexOf("_") + 1), string.Empty) : columnName.Replace("_", string.Empty);
					var clrType = column.Attribute("Type").Value;
					var isNullable = Convert.ToBoolean(column.Attribute("CanBeNull").Value);
					var entityProperty = new EntityPropertyMeta();

					columnName = columnName.Replace("[", "_").Replace("]", string.Empty);
					propertyName = propertyName.Replace("[", "P_").Replace("]", string.Empty);

					entityProperty.ClrType = clrType;
					entityProperty.LinqPropertyName = columnName;
					entityProperty.IsNullable = isNullable;
					entityProperty.JmrPropertyName = propertyName.Substring(0, 1).ToUpper() + propertyName.Substring(1, propertyName.Length - 1);
					entityProperty.IsKey = isKey;

					TranslateClrType(entityProperty);
					functionMeta.Properties.Add(entityProperty);
				}

				ret.Functions.Add(functionMeta);
			}

			return ret;
		}

		private void TranslateClrType(IClrTypeTranslateable propMeta)
		{
			switch (propMeta.ClrType)
			{
				case "System.Boolean":
					propMeta.ClrType = (propMeta.IsNullable) ? "bool?" : "bool";
					break;
				case "System.Byte":
					propMeta.ClrType = (propMeta.IsNullable) ? "byte?" : "byte";
					break;
				case "System.SByte":
					propMeta.ClrType = (propMeta.IsNullable) ? "sbyte?" : "sbyte";
					break;
				case "System.Char":
					propMeta.ClrType = (propMeta.IsNullable) ? "char?" : "char";
					break;
				case "System.Decimal":
					propMeta.ClrType = (propMeta.IsNullable) ? "decimal?" : "decimal";
					break;
				case "System.Double":
					propMeta.ClrType = (propMeta.IsNullable) ? "double?" : "double";
					break;
				case "System.Single":
					propMeta.ClrType = (propMeta.IsNullable) ? "float?" : "float";
					break;
				case "System.Int32":
					propMeta.ClrType = (propMeta.IsNullable) ? "int?" : "int";
					break;
				case "System.UInt32":
					propMeta.ClrType = (propMeta.IsNullable) ? "uint?" : "uint";
					break;
				case "System.Int64":
					propMeta.ClrType = (propMeta.IsNullable) ? "long?" : "long";
					break;
				case "System.UInt64":
					propMeta.ClrType = (propMeta.IsNullable) ? "ulong?" : "ulong";
					break;
				case "System.Object":
					propMeta.ClrType = "object";
					break;
				case "System.Int16":
					propMeta.ClrType = (propMeta.IsNullable) ? "short?" : "short";
					break;
				case "System.UInt16":
					propMeta.ClrType = (propMeta.IsNullable) ? "ushort?" : "ushort";
					break;
				case "System.String":
					propMeta.ClrType = "string";
					break;
				case "System.DateTime":
					propMeta.ClrType = (propMeta.IsNullable) ? "DateTime?" : "DateTime";
					break;
				case "System.Data.Linq.Binary":
					propMeta.ClrType = "System.Data.Linq.Binary";
					break;
				case "System.Guid":
					propMeta.ClrType = (propMeta.IsNullable) ? "System.Guid?" : "System.Guid";
					break;
			}
		}
	}

	public interface IClrTypeTranslateable
	{
		string ClrType { get; set; }
		bool IsNullable { get; set; }
	}

	public class ContextMeta
	{
		public ContextMeta() { }

		public string ContextClassName { get; internal set; }
		public string DatabaseName { get; internal set; }

		public List<EntityMeta> Entities
		{
			get { return _entities; }
		}
		private List<EntityMeta> _entities = new List<EntityMeta>();

		public List<FunctionMeta> Functions
		{
			get { return _functions; }
		}
		private List<FunctionMeta> _functions = new List<FunctionMeta>();
	}

	public class EntityMeta
	{
		public EntityMeta() { }

		public string JmrBusinessClassName { get; internal set; }
		public string LinqEntityClassName { get; internal set; }
		public string LinqEntityCollectionName { get; internal set; }
		public string LinqIdRef { get; internal set; }

		public List<EntityPropertyMeta> Properties
		{
			get { return _properties; }
		}
		private List<EntityPropertyMeta> _properties = new List<EntityPropertyMeta>();

		public List<EntityAssociationMeta> Associations
		{
			get { return _associations; }
		}
		private List<EntityAssociationMeta> _associations = new List<EntityAssociationMeta>();
	}

	public class EntityPropertyMeta : IClrTypeTranslateable
	{
		public EntityPropertyMeta() { }

		public string JmrPropertyName { get; internal set; }
		public string LinqPropertyName { get; internal set; }
		public string ClrType { get; set; }
		public bool IsNullable { get; set; }
		public bool IsKey { get; internal set; }
	}

	public class EntityAssociationMeta
	{
		public EntityAssociationMeta() { }

		public string LinqCollectionMemberName { get; internal set; }
		public string JmrCollectionMemberName { get; internal set; }
		public string LinqEntityName { get; internal set; }
		public string JmrEntityName { get; internal set; }
		public string LinqEntityKeyPropertyName { get; internal set; }
		public bool IsParent { get; internal set; }
	}

	public class FunctionMeta
	{
		public FunctionMeta() { }

		public string LinqMethodName { get; internal set; }
		public string JmrMethodName { get; internal set; }
		public string LinqElementTypeName { get; internal set; }
		public string JmrElementTypeName { get; internal set; }
		public string ClrReturnType { get; internal set; }
		public string LinqEntityIdRef { get; internal set; }

		public List<FunctionParameterMeta> Parameters
		{
			get { return _parameters; }
		}
		private List<FunctionParameterMeta> _parameters = new List<FunctionParameterMeta>();

		public List<EntityPropertyMeta> Properties
		{
			get { return _properties; }
		}
		private List<EntityPropertyMeta> _properties = new List<EntityPropertyMeta>();
	}

	public class FunctionParameterMeta : IClrTypeTranslateable
	{
		public FunctionParameterMeta() { }

		public string LinqParameterName { get; internal set; }
		public string JmrParameterName { get; internal set; }
		public string ClrType { get; set; }
		public bool IsNullable { get; set; }
	}
}
